﻿using System;
using System.IO;
using System.Data;
using System.Data.Sql;
using System.Collections.Generic;
using RFServer;

namespace RFServer.Database
{
    public class DriverStorage : IDisposable
    {
        private CEDriver driver;

        /// <summary>
        /// Конструктор. Автоматически подключается к базе данных. Если файл базы
        /// данных не существует, то он создается и создается сама база данных
        /// </summary>

        public DriverStorage(string DatabasePathFile)
        {
            driver = new CEDriver(DatabasePathFile);

            bool exists = File.Exists(DatabasePathFile);

            if (!exists)
                driver.CreateDatabase();

            driver.Connect();

            if (!exists)
                Create();
        }

        /// <summary>
        /// Создание базы данных
        /// </summary>

        private void Create()
        {
            driver.Execute("CREATE TABLE file_kind (file_kind_id integer not null primary key, kind nvarchar(100) not null, unique(kind))");
            driver.Execute("CREATE TABLE tags (tag_id integer identity not null primary key, sym_name nvarchar(100) not null, file_kind_id integer not null, parent integer, descr nvarchar(300), count integer, unique(sym_name), foreign key(file_kind_id) references file_kind(file_kind_id), foreign key(parent) references tags(tag_id))");
            driver.Execute("CREATE TABLE files (file_id integer identity not null primary key, path nvarchar(256) not null, file_kind_id integer not null, memo nvarchar(600), unique(path), foreign key(file_kind_id) references file_kind(file_kind_id))");
            driver.Execute("CREATE TABLE mm (tag_id integer not null, file_id integer not null, primary key(tag_id, file_id), foreign key(tag_id) references tags(tag_id), foreign key(file_id) references files(file_id))");

            driver.Execute("INSERT INTO file_kind VALUES (0, UPPER('FILE'))");
            driver.Execute("INSERT INTO file_kind VALUES (1, UPPER('DIRECTORY'))");
            driver.Execute("INSERT INTO file_kind VALUES (2, UPPER('CATEGORY'))");
        }

        /// <summary>
        /// Функция ищет id тэгов, названия которых указаны
        /// в массиве keywords
        /// </summary>
        /// <param name="keywords">Запрос пользователя</param>
        /// <param name="start">Индекс слова с которого начинаются в запросе тэги</param>
        /// <param name="end">Индекс первого слова в запросе, которое уже не является тэгом</param>
        /// <returns>Список id искомых тэгов</returns>

        private List<int> GetTagsID(string[] keywords, int start, int end)
        {
            List<int> id_tags = new List<int>();

            for (int i = start; i < end; i ++)
                id_tags.Add(driver.SelectID("select tag_id from tags where sym_name = '" + keywords[i] + "'", "tags"));

            return id_tags;
        }

        /// <summary>
        /// Функция ищет id файлов (объектов), названия которых указаны
        /// в массиве keywords
        /// </summary>
        /// <param name="keywords">Запрос пользователя</param>
        /// <param name="start">Индекс слова с которого начинаются в запросе файлы (объекты)</param>
        /// <param name="end">Индекс первого слова в запросе, которое уже не является файлом (объектов)</param>
        /// <returns>Список id искомых файлов (объектов)</returns>

        private List<int> GetFilesID(string[] keywords, int start, int end)
        {
            List<int> id_tags = new List<int>();

            for (int i = start; i < end; i++)
                id_tags.Add(driver.SelectID("select file_id from files where path = '" + keywords[i] + "'", "files"));

            return id_tags;
        }

        /// <summary>
        /// Функция добавляет новый файл/директорию/категорию в базу данных,
        /// помечая ее тэгами
        /// </summary>
        /// <param name="keywords">Запрос пользователя</param>
        /// <returns>true, если объект успешно добавлен</returns>

        public bool Insert(string[] keywords)
        {
            // Ситаксис
            // INSERT tag1 ... tagN [FILE | DIRECTORY | CATEGORY ] fullpathname

            bool result = true;
            int id_file = 0;
            int i = 0;

            try
            {
                string filename = keywords[keywords.Length - 1];
                string filekind = keywords[keywords.Length - 2];

                // Добавляем тэги

                for (i = 1; keywords[i] != filekind; i++)
                {
                    try
                    {
                        driver.Execute("insert into tags (sym_name, file_kind_id) values ('" + keywords[i] + "', 2)");

                        // "insert into tags (sym_name, file_kind_id) values ('" + keywords[i] + "', " +
                        // " (select file_kind_id from file_kind where kind = 'CATEGORY'))");
                    }
                    catch (EDatabaseObjectAlreadyExists)
                    {
                        // Ничего не делаем, так как тэги добавляются в любом случае
                    }
                }

                // Получаем все идентификаторы добавленных тэгов

                List<int> id_tags = GetTagsID(keywords, 1, i);

                // Отпределяем категорию

                int kind = driver.SelectID("select file_kind_id from file_kind where kind = upper('" + filekind + "')", "kind");

                // Добавляем файл

                try
                {
                    driver.Execute("insert into files (path, file_kind_id) values ('" + filename + "', " + kind + ")");
                }
                catch (EDatabaseObjectAlreadyExists)
                {
                    // Файл уже существует в базе данных
                }

                // Получаем id файла

                id_file = driver.SelectID("select file_id from files where path = '" + filename + "'", "file_id");

                // Удаляем все связки файл-тэг

                driver.Execute("delete from mm where file_id = " + id_file);

                // Добавляем новые связки файл-тэг

                for (i = 0; i < id_tags.Count; i++)
                    driver.Execute("insert into mm values (" + id_tags[i] + ", " + id_file + ")");
            }
            catch (EDatabase e)
            {
                Setup.Log("[database]: INSERT error\n\n" + e.ToString() + "\n");
                result = false;
            }
            catch (IndexOutOfRangeException e)
            {
                Setup.Log("[database]: Wrong INSERT syntax\n\n" + e.ToString() + "\n");
                result = false;
            }
            catch (Exception e)
            {
                Setup.Log("[database]: INSERT unknown error\n\n" + e.ToString() + "\n\n");
                result = false;
            }

            return result;
        }

        /// <summary>
        /// Функция делает выборку файлов/директорий/категорий из базы данных,
        /// которые удовлетворяют указанным в запросе тэгам. Если список тэгов пуст,
        /// то функция вернет все объекты, хранящиеся в базе данных
        /// 
        /// SELECT tag1, ..., tagN
        /// 
        /// </summary>
        /// <param name="keywords">Запрос пользователя</param>
        /// <returns>Список объектов или null в случае ошибки</returns>

        public List<string> Select(string[] keywords)
        {
            List<string> result = new List<string>();

            try
            {
                if (keywords.Length == 1)
                {
                    // Формируем запрос
                    // SELECT files.path, files.file_kind_id FROM files

                    string select = "select files.path, files.file_kind_id from files";

                    // Выполняем запрос. Результат - список файлов, которые
                    // удовлетворяют всем найденным тэгам

                    DataTable files = driver.Select(select, "files");

                    // Извлекаем найденные файлы в массив
                    // file1?type1|file2?type2|...

                    for (int i = 0; i < files.Rows.Count; i++)
                        result.Add(files.Rows[i][0].ToString() + "?" + files.Rows[i][1].ToString());
                }
                else
                {
                    // Формируем запрос
                    // SELECT files.path, files.file_kind_id FROM mm
                    // JOIN files on files.file_id = mm.file_id
                    // WHERE mm.tag_id = 1 AND mm.file_id IN
                    // (SELECT mm.file_id FROM mm WHERE mm.tag_id = 2)

                    int tags_count = keywords.Length - 1;

                    List<int> tags_id = GetTagsID(keywords, 1, keywords.Length);
                    string select = "select files.path, files.file_kind_id from mm join files on files.file_id = mm.file_id " +
                        "where mm.tag_id = " + tags_id[0] + " ";

                    if (tags_count > 1)
                    {
                        for (int i = 1; i < tags_id.Count; i++)
                            select += "and mm.file_id in (select mm.file_id from mm where mm.tag_id = " + tags_id[i] + " ";

                        for (int i = 1; i < tags_id.Count; i++)
                            select += ")";
                    }

                    // Выполняем запрос. Результат - список файлов, которые
                    // удовлетворяют всем найденным тэгам

                    DataTable files = driver.Select(select, "files");

                    // Извлекаем найденные файлы в массив
                    // file1?type1|file2?type2|...

                    for (int i = 0; i < files.Rows.Count; i++)
                        result.Add(files.Rows[i][0].ToString() + "?" + files.Rows[i][1].ToString());

                    // 
                }
            }
            catch (EDatabaseTableEmpty)
            {
                // Тэг, указанный пользователем, не содержится в базе данных
                // Функция вернет пустой список result
            }
            catch (EDatabase e)
            {
                Setup.Log("[database]: SELECT error\n\n" + e.ToString() + "\n");
                result = null;
            }
            catch (IndexOutOfRangeException e)
            {
                Setup.Log("[database]: SELECT wrong syntax\n\n" + e.ToString() + "\n");
                result = null;
            }
            catch (ArgumentOutOfRangeException e)
            {
                Setup.Log("[database]: SELECT wrong  syntax\n\n" + e.ToString() + "\n");
                result = null;
            }
            catch (Exception e)
            {
                Setup.Log("[database]: SELECT unknown error\n\n" + e.ToString() + "\n");
                result = null;
            }

            return result;
        }

        /// <summary>
        /// Функция переименовывает файл/категорию в базе данных
        /// 
        /// RENAME [OBJECT | CATEGORY] object_name1 object_name2
        /// 
        /// </summary>
        /// <param name="keywords">Запрос пользователя</param>
        /// <returns>true, если объект успешно переименован</returns>

        public bool Rename(string[] keywords)
        {
            bool result = true;

            try
            {
                // Формируем данные для запроса

                string kind = keywords[1].ToUpper();
                string o1 = keywords[2];
                string o2 = keywords[3];

                // Переименовываем объекты из базы данных

                if (kind == "OBJECT")
                {
                    // Переименование FILE, DIRECTORY из таблицы FILES

                    driver.Execute("update files set files.path = '" + o2 +
                            "' where upper(files.path) = upper('" + o1 + "')");
                }
                else if (kind == "CATEGORY")
                {
                    // Переименование CATEGORY из таблицы TAGS

                    driver.Execute("update tags set tags.sym_name = '" + o2 +
                            "' where upper(tags.sym_name) = upper('" + o1 + "')");
                }
                else
                    throw new IndexOutOfRangeException("Syntax: RENAME [OBJECT | CATEGORY] object_name1 object_name2");
            }
            catch (EDatabase e)
            {
                Setup.Log("[database]: RENAME error\n\n" + e.ToString() + "\n");
                result = false;
            }
            catch (IndexOutOfRangeException e)
            {
                Setup.Log("[database]: RENAME wrong syntax\n\n" + e.ToString() + "\n");
                result = false;
            }
            catch (Exception e)
            {
                Setup.Log("[database]: RENAME unknown error\n\n" + e.ToString() + "\n");
                result = false;
            }

            return result;
        }

        /// <summary>
        /// Функция возвращает все тэги из базы данных. Если при этом в запросе указан
        /// конкретный файл, то возвращается список только его тэгов
        /// </summary>
        /// <param name="keywords">Запрос пользователя</param>
        /// <returns>Список тэгов или null в случае ошибки</returns>

        public List<string> Tags(string[] keywords)
        {
            List<string> result = new List<string>();

            try
            {
                // Генeрация SELECT-запроса
                // Если filename == null, то выводим все тэги из базы данных
                // Если filename != null, то выводим список тэгов для конкретного файла

                string select = "";

                if (keywords == null)
                    select = "select tags.sym_name, tags.file_kind_id from tags order by tags.sym_name";
                else
                    select = "select tags.sym_name, tags.file_kind_id from tags " +
                             "join mm on mm.tag_id = tags.tag_id " +
                             "join files on files.file_id = mm.file_id " +
                             "where files.path = '" + keywords[1] + "' " + 
                             "order by tags.sym_name";

                DataTable tags = driver.Select(select, "tags");

                // Извлекаем найденные тэги в массив
                // tag1?tag_type|tag2?tag_type|...

                for (int i = 0; i < tags.Rows.Count; i++)
                    result.Add(tags.Rows[i][0].ToString() + "?" + tags.Rows[i][1].ToString());
            }
            catch (EDatabaseTableEmpty)
            {
                // Список тэгов пуст
                // Функция вернет пустой список result
            }
            catch (EDatabase e)
            {
                Setup.Log("[database]: SHOW TAGS error\n\n" + e.ToString() + "\n");
                result = null;
            }
            catch (Exception e)
            {
                Setup.Log("[database]: SHOW TAGS unknown error\n\n" + e.ToString() + "\n");
                result = null;
            }

            return result;
        }

        /// <summary>
        /// Функция проверяет, существует ли тэг с названием tag
        /// </summary>
        /// <param name="tag">Название тэга</param>
        /// <returns>true, если тэг существует</returns>

        public bool TagExists(string tag)
        {
            try
            {
                string select = "select tag_id from tags where sym_name = '" + tag + "'";

                // Выполняем запрос. Результат - таблица, содержащая id
                // запрашиваемого тэга (может быть пустой)

                driver.Select(select, "tags");

                // Таблица не пустая, т.к. не возникло исключения
                // Значит тэг существует в базе данных

                return true;
            }
            catch (EDatabaseTableEmpty)
            {
                // Тэг, указанный пользователем, не содержится в базе данных
            }
            catch (EDatabase e)
            {
                Setup.Log("[database]: Tag exists error\n\n" + e.ToString() + "\n");
            }
            catch (Exception e)
            {
                Setup.Log("[database]: Tag exists unknown error\n\n" + e.ToString() + "\n");
            }

            // Если мы попали сюда, значит произошло исключение

            return false;
        }

        /// <summary>
        /// Функция удаляет все тэги, к которым не привязан ни один объект из базы
        /// данных
        /// </summary>
        /// <returns>true в случае успеха</returns>

        public bool RemoveUnusedTags()
        {
            // Формируем запрос
            // SELECT sym_name FROM tags
            // WHERE NOT EXISTS (SELECT tag_id FROM mm WHERE mm.tag_id = tags.tag_id)

            bool result = true;

            try
            {
                driver.Execute("delete from tags where not exists " +
                    "(select tag_id from mm where mm.tag_id = tags.tag_id)");
            }
            catch (EDatabase e)
            {
                Setup.Log("[database]: Remove unused tags error\n\n" + e.ToString() + "\n");
                result = false;
            }
            catch (Exception e)
            {
                Setup.Log("[database]: Remove unused tags unknown error\n\n" + e.ToString() + "\n\n");
                result = false;
            }

            return result;
        }

        /// <summary>
        /// Функция удаляет все объекты, у которых свойство files.path удовлетворяет
        /// написанному в аргументах команды
        /// 
        /// DELETE [OBJECT | CATEGORY] object1 object 2 ...
        /// 
        /// </summary>
        /// <param name="keywords">Запрос пользователя</param>
        /// <returns>true в случае успеха</returns>

        public bool Delete(string[] keywords)
        {
            bool result = true;

            try
            {
                // Формируем запросы

                string kind = keywords[1].ToUpper();

                // Удаляем объекты из базы данных

                if (kind == "OBJECT")
                {
                    List<int> files_ids = new List<int>();

                    try
                    {
                        // Удаление FILE, DIRECTORY или CATEGORY из таблицы FILES
                        // Получаем список id удаляемых файлов

                        files_ids = GetFilesID(keywords, 2, keywords.Length);
                    }
                    catch (EDatabaseTableEmpty)
                    {
                        // Таких объектов в таблице нет, список files_ids будет пуст
                    }

                    // Удаляем все элементы из базы данных, которые затребовал пользователь

                    for (int i = 0; i < files_ids.Count; i++)
                    {
                        driver.Execute("delete from mm where file_id = " + files_ids[i]);
                        driver.Execute("delete from files where file_id = " + files_ids[i]);
                    }
                }
                else if (kind == "CATEGORY")
                {
                    List<int> tags_ids = new List<int>();

                    try
                    {
                        // Удаление CATEGORY
                        // Получаем список id удаляемых файлов

                        tags_ids = GetTagsID(keywords, 2, keywords.Length);
                    }
                    catch (EDatabaseTableEmpty)
                    {
                        // Таких категорий в таблице нет, список files_ids будет пуст
                    }

                    // Удаляем все элементы из базы данных, которые затребовал пользователь
                    // При этом, если категория являлась одновременно и объектом (файлом) в
                    // таблице files, то следует удалить ее как объект (файл)

                    for (int i = 0; i < tags_ids.Count; i++)
                    {
                        // Удаляем категорию

                        driver.Execute("delete from mm where tag_id = " + tags_ids[i]);
                        driver.Execute("delete from tags where tag_id = " + tags_ids[i]);

                        try
                        {
                            // Удаляем все записи, в которых данную категорию помечали другими категориями

                            int object_id = driver.SelectID("select file_id from files where path = '" + keywords[2 + i] + "'", "files");

                            driver.Execute("delete from mm where file_id = " + object_id);
                            driver.Execute("delete from files where file_id = " + object_id);
                        }
                        catch (EDatabaseTableEmpty)
                        {
                            // Ничего делать не нужно, т.к. данную категорию другими категориями не помечали,
                            // и естественно, что таблица пустая
                        }
                    }
                }
                else
                    throw new IndexOutOfRangeException("Syntax: DELETE [OBJECT | CATEGORY] object1 object 2 ...");

                // Если образовались пустые тэги, что очищаем их

                if (Setup.AutomaticallyRemoveEmptyTags)
                    result = RemoveUnusedTags();
            }
            catch (EDatabase e)
            {
                Setup.Log("[database]: DELETE error\n\n" + e.ToString() + "\n");
                result = false;
            }
            catch (IndexOutOfRangeException e)
            {
                Setup.Log("[database]: DELETE wrong syntax\n\n" + e.ToString() + "\n");
                result = false;
            }
            catch (Exception e)
            {
                Setup.Log("[database]: DELETE unknown error\n\n" + e.ToString() + "\n");
                result = false;
            }

            return result;
        }

        /// <summary>
        /// Функция полностью очищает все таблицы базы данных и создает ее заново
        /// </summary>
        /// <returns>true в случае успеха</returns>

        public bool ClearDatabase()
        {
            try
            {
                driver.Execute("drop table mm");
                driver.Execute("drop table files");
                driver.Execute("drop table tags");
                driver.Execute("drop table file_kind");

                Create();
            }
            catch (EDatabase e)
            {
                Setup.Log("[database]: CLEAR DATABASE error:\n\n" + e.ToString() + "\n\n");
                return false;
            }
            catch (Exception e)
            {
                Setup.Log("[database]: CLEAR DATABASE unknown error:\n\n" + e.ToString() + "\n\n");
                return false;
            }

            return true;
        }

        /// <summary>
        /// Отсоединение от базы данных
        /// </summary>

        public void Disconnect()
        {
            driver.Disconnect();
        }

        /// <summary>
        /// Реализация деструктора
        /// </summary>

        public void Dispose()
        {
            Disconnect();
        }
    }
}
